iTesting软件测试知识分享

你不知道的Cypress系列(2) -- 该死的PO模型​

自从我的新书 前端自动化测试框架 – Cypress从入门到精通上市以来,这本书受到了大量同学热情的追捧和讨论。在跟同学们的交流中,我也了解到, 原来除了国外优秀的公司(例如Adobe, 迪士尼,AutoDesk等等), 国内也有很多公司在尝试使用Cypress提升测试效率。而在Cypress中国群内、在公众号iTesting里,我每天都能看到大量关于Cypress的使用讨论和私下问询。这让我感到无比荣幸。(买了书的同学们,公众号回复你的微信,拉你到Cypress中国群)。

除了日常推荐大家通过阅读我的书来解决日常Cypress使用问题外,我也一直在更新着我这边的Cypress知识图谱, 不夸张的说,目前我总结和实践下来知识点多达200多篇。本着“雕琢自我,普惠他人”的原则,我决定在公众号iTesting上开设<你不知道的Cypress系列>专栏。此专栏目的是分享一些我自己趟过的坑,走过的弯路、以及在选型时抛弃了的实践。希望让大家在选用Cypress作为前端自动化测试框架方案时, 可以借鉴一下,避免再走我走过的弯路。

今天是<你不知道的Cypress系列>的第二篇 – 绝知此事要躬行,别被Cypress官方忽悠瘸了!

为了让大家看到标题就知道我再说什么,我把标题更改为:

你不知道的Cypress系列(2) – ”该死”的PO模型

01 PO模型是什么

无论你基于何种自动化测试框架开发你的测试脚本,PO模型绝对是你绕不过的坎儿。

PO模型(Page Object Module)算得上自动化测试的最佳实践之一,其中心思想如下:

1
2
3
4
5
把物理上的页面或者逻辑上的功能组合当成一个Page 类处理。
针对每一个Page类,将此Page上所属的元素、此Page类上元素动作的组合分别封装成Object, 以及Class Methods。
所有针对此页面的操作以Page 类的实例引用。

从代码实现上来看,元素、元素操作、 Page类、Page类对应的测试类就是PO。

实现PO模型后,测试用例的操作细节会被隐藏,转而以面向对象,或者说,以业务角度展示操作步骤,我们直接看一个PO封装后的测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import LoginPage from "../pages/login"
import mainPage from "../pages/main"
describe('PageObject模式之登录测试', function () {
// 关注公众号iTesting,玩转Cypress
const username = 'iTesting'
const password = 'password123'
it('登录成功', function () {
const loginInstance = new LoginPage()
loginInstance.visitPage()
.isTargetPage()
.login(username, password)
const mainInstance = new mainPage()
mainInstance.isTargetPage()
.welComeText.should('contain', 'iTesting')
})
})

从业务角度看,PO模型非常直观:

1
2
3
4
5
6
7
8
初始化LoginPage实例
访问LoginPage
判断LoginPage可访问
登录
接着访问mainPage(登录后会跳转的页面)
判断mainPage可访问
在mainPage上断言

02 PO模型的好处

由上文可以看到, PO模型的目的,主要是为了重用元素,做到每个元素定位、每个元素、甚至每个类方法,在整个项目中,有且仅有一处定义,其它都是调用。通过这样的方式,PO模型做到了即使在复杂项目中,也不会增加维护成本。

除此之外,在当前微服务开发模式下,动辄十几个、几十个微服务会同步进行开发。那么,过去那种一个测试工程师搞定所有自动化测试的机会不再有了。当前大多数公司的实践是将测试框架收归专门团队负责,而将自动化脚本的编写下放到各微服务团队。对应的, 各个团队下的业务测试工程师要具体负责其微服务的自动化测试。

PO模型天然带来一个好处,即,Page类天然隔离了模块和团队。例如我是团队A的测试工程师,除去公用Page外,我只需要关注我这个微服务下的所有Page类及类方法即可。而不必关心其它团队所own的页面。

如果我对其它组的服务有依赖,这些通常会构建专门的函数并成为Common Page的一部分。

如果有新人进来,他的学习成本只是我们团队负责的页面 + 公用Page,相对来说比较友好。

03 Cypress怎么看PO模型

正如Cypress官方所宣扬的一样:

1
// The page object pattern isn’t actually anything “special”.

换句话说, Page Object只是方便重用而已,没什么大不了。

Cypress官方觉得Page Object模型里的大量Page类及其对应的测试类的使用,会加重调用链条,隐藏各个操作之间的动作细节,加重使用者的负担, 具体来说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. 使用PO模型人为的在测试中引入了其他状态,这些状态是你(测试脚本创建者)自己定义的,而不是应用程序内部拥有的, 它增加了debug成本。
// 这个假设是成立的,因为程序内部并没有Page,更遑论Page里的方法。
// 那么当你运行失败时发现,Page.addWallet失败了,你无法直接知道哪里出错
// 你必须找到addWallet的定义,再去查看其实现,才能知道哪里错。
2. 使用PO模型使得测试速度变慢。
// 这也是事实。
// 毕竟你每次操作都要先initial Page实例,然后再寻找类方法,最后才是执行。
3. 使用PO模型使代码陷入“Conditional Testing”的怪圈。
// 比如你的方法里存在如下判断:
// 如果页面发生AAA, 你会进行BBB操作, 如果发生CCC,你会进行DDD操作。
// 这在Cypress看来是反模式。因为Cypress跟你的应用程序运行在同一个生命周期。
// Cypress可以捕获应用程序里发生的一切。
// 所以,你理应知道你的操作引发的结果到底是AAA还是CCC。

这3条, 条条剑指PO模型的七寸。

Cypress官方又说,好既然PO不好用, 而且它存在只是为了方便重用,那么我给你更好的办法:

于是Custom Commands出炉了。Custom Commands你可以看成是PO模型里的Common Page。所有在Custom Commands里定义的方法,天生可以被任何测试之间调用。相当于你生成了自己的全局命令.

来看一个Custom Commands的具体例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Cypress.Commands.add('login', (username, password) => {
Cypress.log({
name: 'login',
message: `${username} | ${password}`,
})
// 关注公众号iTesting,精通Cypress使用
return cy.request({
method: 'POST',
url: '/login',
form: true,
body: {
username,
password,
},
})
})

这样,在使用上,你也不需要管什么Login Page类,Login测试类了。你在任何代码里直接写:

1
cy.login("关注iTesting,玩转Cypress")

它自然帮你登录成功, Page是什么?顿时不香了好吗!

Custom Commands的具体用法我的新书<前端自动化测试框架 – Cypress从入门到精通>里讲的还算通透,这里就不多说。

另外,在JavaScript世界里, 很讲究一个链式调用(Chainable), Custom COmmands + 链式调用,Cypress认为它完全可以取代PO模型。

通过chainable把你的所有操作“可视化”。于是,一个Cypress推崇的测试用例就变成这样:

1
2
3
4
5
6
7
8
9
10
11
/// <reference types="cypress" />
describe('Custom Commands模式之登录测试', function () {
// 关注公众号iTesting,玩转Cypress
it('登录成功', function () {
cy.login(username, password)
.verifyLoginSuccess()
.verifyWelcomeTxt()
})
})

从cypress角度,你看到的是login成功后直接去验证welcome的文本在不在。

这样感觉代码量是不是更少,代码更直观了?

04 我怎么看PO和Custom Commands?

这里我也谈下我对PO和Custom Commands的看法。

首先要谨记:Cypress的出现是为Developer服务的,它对Developer的友好程度要高于Tester。

基于此,我们再来看登录成功这个用例, 你看到的是

1
2
3
cy.xxx()
.yyy()
.zzz()

这样的模式调用链清晰,从层次上来是,也比PO模型少了一层。出错后的调试,也更方便。

但是!

你的测试用例都是cy这样,cy那样,当然对于Cypress官方来说,很成功,Visibility非常高,简直是Cypress的活广告,美滋滋啊!

可是,你的“业务”呢?

不错!你的业务以及业务细节被隐藏了!

虽然从Cypress的Custom Commands方式让测试写起代码来更爽,但是别忘记,在国内,我们还存在大量的测试人员,测试开发水平不足!(此时应该有广告,我的拉勾专栏<测试开发入门与实战>开栏24小时内售卖超10000+, 破了测试专栏的记录,值得你去拉勾上搜一下 :))

而且,从习惯上来说,国内的同学们更习惯从业务角度去理解测试。并且Custom Commands把所有的公用功能都写在一个文件里,对测试人员来说不友好,当我的测试用例超过10000条时,Custom Commands里的公用方法,恐怕也有几百个了。总不能我来一个新人,让他花上几周时间去熟悉所有的方法吧!况且,都微服务了,他以后基本也只负责其中一些测试,那么我让他学那么多跟他没关系的方法干嘛呢?

虽然Custom Commands也可以做到按照微服务组织,然后在每个微服务Folder下实现一个Custom Commands的子域。但是,这样做,你使用时候由要跟PO模型一样先引入再使用, 那么它跟PO模型又有什么区别呢?!!

05 PO和Custom Commands我都要

纸上得来终觉浅!
纸上得来终觉浅!
纸上得来终觉浅!

不能迷信权威!我刚开始搭建公司的前端框架时, 我就完全按照Cypress官方建议做,结果,当我的测试用例到达几千条时,我傻了,Custom Commands里的方法几百个,即使是我自己写的,但我自己也闹不清楚哪个做哪个用处,因为没有了Page做参考,时间一长,我很难从函数命名上看出这个方法应该在那个页面下使用, 更别说对框架不熟悉的新人了。

结果没办法,我重新返工了一遍,把特别核心的公用功能放到Custom Commands里,把跟业务有关的,还是以Page Object方式组织。放到微服务下,放到Page下,这样, 再来新人,我告诉他,你只要看这个commands这个文件还有你的微服务folder就好了。

所以, PO + Custom Commands + chainable是我的最佳实践, 以后我的测试用例就变成这样了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <reference types="cypress" />
import mainPage from "../pages/main"
describe('PageObject模式之登录测试', function () {
// 关注公众号iTesting,玩转Cypress
const username = 'iTesting'
const password = 'password123'
it('登录成功', function () {
cy.login(username, password)
const mainInstance = new mainPage()
mainInstance.isTargetPage()
.welComeText.should('contain', 'iTesting')
})
})

这下终于清静了!是么?

Cypress又提出了一个模型,App Actions, 同学,你想去尝尝鲜吗?
买了书的同学们,公众号回复你的微信,拉你到Cypress中国群)。

往期回看:
你不知道的Cypress系列(1) –鸡肋的BDD
下期预告:
你不知道的Cypress系列(3) – 是时候重构自己的思维了!

🐶 您的支持将鼓励我继续创作 🐶
-------------评论, 吐槽, 学习交流,请关注微信公众号 iTesting-------------
请关注微信公众号 iTesting wechat
扫码关注,跟作者互动

本文标题:你不知道的Cypress系列(2) -- 该死的PO模型​

文章作者:请关注微信公众号 iTesting

发布时间:2020年12月30日 - 23:12

最后更新:2021年12月10日 - 23:12

原始链接:http://www.helloqa.com/2020/12/30/你不知道的Cypress系列/你不知道的Cypress系列(2) -- 该死的PO模型​/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。